/*
 * Copyright (c) 2012 swing-on-fire Team
 * 
 * This file is part of Swing-On-Fire (http://code.google.com/p/swing-on-fire), licensed under the terms of the MIT
 * License (MIT).
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.google.code.swingonfire.showcase.interpolation;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.JComponent;

import com.google.code.swingonfire.animation.Animation;
import com.google.code.swingonfire.animation.AnimationLoop;
import com.google.code.swingonfire.animation.Animator;
import com.google.code.swingonfire.image.ImageCache;
import com.google.code.swingonfire.utility.Colors;
import com.google.code.swingonfire.utility.interpolation.FixInterpolation;
import com.google.code.swingonfire.utility.interpolation.Interpolation;

public class InterpolationComponent extends JComponent
{

    private static class HistoricLine
    {
        private final Line2D line;
        private final long nanos;

        public HistoricLine(Line2D line, long nanos)
        {
            super();
            this.line = line;
            this.nanos = nanos;
        }

        public Line2D getLine()
        {
            return line;
        }

        public double lifetime(long nanos)
        {
            return (nanos - this.nanos) / 1000000000d;
        }

        public boolean isOutdated(long nanos, double maxLifetime)
        {
            return lifetime(nanos) > maxLifetime;
        }

        public Color getColor(Color color, long nanos, double maxLifetime)
        {
            return Colors.translucent(color, 1 - (lifetime(nanos) / maxLifetime));
        }

    }

    private static final long serialVersionUID = 5353004632785835433L;

    private static final Image POINTER = ImageCache.getImage("pointer");
    private static final Stroke MAIN_GRID_STROKE = new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
    private static final Stroke MINOR_GRID_STROKE = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
    private static final Stroke MINEST_GRID_STROKE = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
        10.0f, new float[]{1.0f, 4.0f}, 0.0f);
    private static final Stroke CURVE_STROKE = new BasicStroke(3);

    private final List<HistoricLine> lines = new ArrayList<HistoricLine>();
    private final Rectangle2D preferredRectangle = new Rectangle2D.Double(-0.25, -0.25, 1.5, 1.5);
    private final Rectangle2D viewRectangle = new Rectangle2D.Double(-0.25, -0.25, 1.5, 1.5);

    private Color gridColor = Color.GRAY;
    private Color curveColor = Color.BLUE;
    private long startNanos = 0;
    private double duration = 1;

    private Interpolation interpolation;
    private Animation animation;

    private Point2D position;
    private Point2D lastPosition;

    private boolean running;

    public InterpolationComponent()
    {
        super();

        setPreferredSize(new Dimension(640, 480));

        setInterpolation(null);
    }

    public Color getGridColor()
    {
        return gridColor;
    }

    public void setGridColor(Color gridColor)
    {
        this.gridColor = gridColor;
    }

    public Color getCurveColor()
    {
        return curveColor;
    }

    public void setCurveColor(Color curveColor)
    {
        this.curveColor = curveColor;
    }

    public void setInterpolation(Interpolation interpolation)
    {
        if (isRunning())
        {
            Animator.stop(animation);
        }

        reset(duration);

        if (interpolation == null)
        {
            interpolation = new FixInterpolation();
        }

        this.interpolation = interpolation;

        preferredRectangle.setRect(-0.25, interpolation.getMinimum() - 0.25, duration + 0.5,
            (interpolation.getMaximum() - interpolation.getMinimum()) + 0.5);
        animation = AnimationLoop.loop(new InterpolationAnimation(duration, interpolation, this));

        if (isRunning())
        {
            play();
        }
    }

    public void reset(double duration)
    {
        startNanos = System.nanoTime();
        this.duration = duration;

        setPosition(0);
    }

    public void setPosition(double y)
    {
        lastPosition = position;
        position = new Point2D.Double((System.nanoTime() - startNanos) / 1000000000d, y);

        if ((lastPosition != null) && (position.getX() > lastPosition.getX()))
        {
            lines.add(new HistoricLine(new Line2D.Double(lastPosition, position), System.nanoTime()));
        }

        //        updatePreferredRectangle();

        repaint();
    }

    public void updatePreferredRectangle()
    {
        long nanos = System.nanoTime();
        Iterator<HistoricLine> iterator = lines.iterator();

        preferredRectangle.setRect(0, 0, 1, 1);

        while (iterator.hasNext())
        {
            HistoricLine line = iterator.next();

            if (line.isOutdated(nanos, duration))
            {
                iterator.remove();
            }
            else
            {
                preferredRectangle.add(duration, line.getLine().getP2().getY());
            }
        }

        preferredRectangle.setRect(Math.max(-5, Math.min(0, preferredRectangle.getX())) - 0.25,
            Math.max(-5, Math.min(0, preferredRectangle.getY())) - 0.25,
            Math.min(10, preferredRectangle.getWidth()) + 0.5, Math.min(10, preferredRectangle.getHeight()) + 0.5);
    }

    public void updateViewRectangle()
    {
        double minX = viewRectangle.getMinX();
        double maxX = viewRectangle.getMaxX();
        double minY = viewRectangle.getMinY();
        double maxY = viewRectangle.getMaxY();
        double minXCorrection = preferredRectangle.getMinX() - minX;
        double maxXCorrection = preferredRectangle.getMaxX() - maxX;
        double minYCorrection = preferredRectangle.getMinY() - minY;
        double maxYCorrection = preferredRectangle.getMaxY() - maxY;
        boolean update = false;

        if (Math.abs(minXCorrection) > 0.01)
        {
            minX += minXCorrection * 0.05;
            update = true;
        }

        if (Math.abs(maxXCorrection) > 0.01)
        {
            maxX += maxXCorrection * 0.05;
            update = true;
        }

        if (Math.abs(minYCorrection) > 0.01)
        {
            minY += minYCorrection * 0.05;
            update = true;
        }

        if (Math.abs(maxYCorrection) > 0.01)
        {
            maxY += maxYCorrection * 0.05;
            update = true;
        }

        if (update)
        {
            viewRectangle.setRect(minX, minY, maxX - minX, maxY - minY);
        }
    }

    private Point2D convert(double x, double y, int width, int height)
    {
        return new Point2D.Double(convertWidth(x - viewRectangle.getMinX(), width), height
            - convertHeight(y - viewRectangle.getMinY(), height));
    }

    private double convertWidth(double w, int width)
    {
        return w * (width / viewRectangle.getWidth());
    }

    private double convertHeight(double h, int height)
    {
        return h * (height / viewRectangle.getHeight());
    }

    public boolean isRunning()
    {
        return running;
    }

    public void play()
    {
        Animator.start(animation);
        running = true;
        repaint();
    }

    public void pause()
    {
        // TODO Auto-generated method stub

        repaint();
    }

    public void stop()
    {
        Animator.stop(animation);
        running = false;
        repaint();
    }

    @Override
    protected void paintComponent(Graphics graphics)
    {
        updateViewRectangle();

        int width = getWidth();
        int height = getHeight();

        Graphics2D g = (Graphics2D) graphics.create();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF);

        if (isOpaque())
        {
            g.setColor(getBackground());
            g.fillRect(0, 0, width, height);
        }

        drawGrid(g, width, height);
        drawLines(g, width, height);

        if (isRunning())
        {
            Point2D pointerPosition = convert(position.getX(), position.getY(), width, height);

            graphics.drawImage(POINTER, (int) (pointerPosition.getX() - (POINTER.getWidth(this) / 2)),
                (int) pointerPosition.getY(), this);
        }

        super.paintComponent(graphics);
    }

    private void drawLines(Graphics2D g, int width, int height)
    {
        g.setStroke(CURVE_STROKE);
        long nanos = System.nanoTime();
        Line2D.Double currentLine = new Line2D.Double();

        for (HistoricLine line : lines)
        {
            g.setColor(line.getColor(getCurveColor(), nanos, duration));
            currentLine.setLine(convert(line.getLine().getX1(), line.getLine().getY1(), width, height),
                convert(line.getLine().getX2(), line.getLine().getY2(), width, height));
            g.draw(currentLine);
        }
    }

    private void drawGrid(Graphics2D g, int width, int height)
    {
        g.setColor(getGridColor());
        g.setStroke(MAIN_GRID_STROKE);
        g.setFont(getFont());

        FontMetrics fontMetrics = g.getFontMetrics();

        Point2D tl = convert(viewRectangle.getMinX() + 0.125, viewRectangle.getMinY() + 0.125, width, height);
        Point2D zr = convert(0, 0, width, height);
        Point2D br = convert(viewRectangle.getMaxX() - 0.125, viewRectangle.getMaxY() - 0.125, width, height);
        Line2D.Double hLine = new Line2D.Double(tl, br);

        double xMult = convertWidth(1, width);
        double yMult = convertHeight(1, height);
        double x = Math.round((viewRectangle.getMinX() + 0.25) * 10) / 10d;
        double y = Math.round((viewRectangle.getMaxY() - 0.25) * 10) / 10d;

        while (x < (viewRectangle.getMaxX() - 0.2))
        {
            float lx = (float) (zr.getX() + (x * xMult));
            float ly0 = (float) (tl.getY());
            float ly1 = (float) (br.getY());

            if (Math.abs(Math.round(x * 2) - (x * 2)) < 0.01)
            {
                g.setColor(getGridColor());

                if (Math.abs(x) < 0.01)
                {
                    g.setStroke(MAIN_GRID_STROKE);

                    hLine.setLine(lx, ly1, lx - 4, ly1 + 4);
                    g.draw(hLine);
                    hLine.setLine(lx, ly1, lx + 4, ly1 + 4);
                    g.draw(hLine);
                    hLine.setLine(lx, ly0, lx, ly1);
                }
                else
                {
                    if (Math.abs(Math.round(x) - x) < 0.01)
                    {
                        g.setStroke(MAIN_GRID_STROKE);
                    }
                    else
                    {
                        g.setStroke(MINOR_GRID_STROKE);
                    }

                    String s = String.format("%,.1f", x);

                    g.drawString(s, lx - (fontMetrics.stringWidth(s) / 2), ly1 + fontMetrics.getDescent());
                    hLine.setLine(lx, ly0, lx, ly1 + fontMetrics.getAscent());
                }

                g.draw(hLine);
            }
            else if (xMult > 200)
            {
                g.setColor(Colors.translucent(getGridColor(), 0.5));
                g.setStroke(MINEST_GRID_STROKE);

                hLine.setLine(lx, ly0, lx, ly1);
                g.draw(hLine);
            }

            x += 0.1;
        }

        while (y > (viewRectangle.getMinY() + 0.125))
        {
            float lx0 = (float) (tl.getX());
            float lx1 = (float) (br.getX());
            float ly = (float) (zr.getY() - (y * yMult));

            if (Math.abs(Math.round(y * 2) - (y * 2)) < 0.01)
            {
                g.setColor(getGridColor());

                if (Math.abs(y) < 0.01)
                {
                    g.setStroke(MAIN_GRID_STROKE);

                    hLine.setLine(lx1, ly, lx1 - 4, ly - 4);
                    g.draw(hLine);
                    hLine.setLine(lx1, ly, lx1 - 4, ly + 4);
                    g.draw(hLine);

                    Graphics2D gText = (Graphics2D) g.create();
                    gText.translate(lx1, ly);
                    gText.rotate(Math.PI / 2);
                    gText.drawString("time", -fontMetrics.stringWidth("time") / 2, -fontMetrics.getDescent());
                }
                else if (Math.abs(Math.round(y) - y) < 0.01)
                {
                    g.setStroke(MAIN_GRID_STROKE);
                }
                else
                {
                    g.setStroke(MINOR_GRID_STROKE);
                }

                String s = String.format("%,.1f", y);
                int stringWidth = fontMetrics.stringWidth(s);

                g.drawString(s, lx0 - 4, (ly + (fontMetrics.getDescent() / 2)));
                hLine.setLine(lx0 + stringWidth, ly, lx1, ly);
                g.draw(hLine);

            }
            else if (yMult > 100)
            {
                g.setColor(Colors.translucent(getGridColor(), 0.5));
                g.setStroke(MINEST_GRID_STROKE);

                hLine.setLine(lx0, ly, lx1, ly);
                g.draw(hLine);
            }

            y -= 0.1;
        }
    }

}
